﻿using System;
using System.Reflection.Emit;
using System.Reflection;

namespace Rsdn.SmartApp
{
	partial class ServicesHelper
	{
		private class AssignHelper
		{
			private static readonly ElementsCache<Type, AssignDelegate> _assignMethods =
				new ElementsCache<Type, AssignDelegate>(CreateAssignMethod);
			private static readonly MethodInfo _getServiceMethod =
				typeof(IServiceProvider).GetMethod("GetService");
			private static readonly MethodInfo _getRequiredServiceMethod =
				typeof(ServicesHelper).GetMethod("GetRequiredService", new Type[] {typeof (IServiceProvider)});

			private static AssignDelegate CreateAssignMethod(Type type)
			{
				var method = new DynamicMethod(
					"AssignMethod@" + type.FullName,
					null,
					new Type[] { typeof (IServiceProvider), typeof (object)},
					true);

				var gen = method.GetILGenerator();

				gen.DeclareLocal(type);

				gen.Emit(OpCodes.Ldarg_1);
				gen.Emit(OpCodes.Castclass, type);
				gen.Emit(OpCodes.Stloc_0);

				foreach (var info in type.GetFields(
						BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
				{
					var attrs = (ExpectServiceAttribute[])info.GetCustomAttributes(
						typeof(ExpectServiceAttribute), true);
					if (attrs.Length > 0)
					{
						gen.Emit(OpCodes.Ldloc_0);
						gen.Emit(OpCodes.Ldarg_0);

						if (!attrs[0].Required)
							gen.Emit(OpCodes.Callvirt, _getServiceMethod.MakeGenericMethod(info.FieldType));
						else
							gen.Emit(OpCodes.Call, _getRequiredServiceMethod.MakeGenericMethod(info.FieldType));
						gen.Emit(OpCodes.Stfld, info);
					}
				}

				gen.Emit(OpCodes.Ret);

				return (AssignDelegate)method.CreateDelegate(typeof(AssignDelegate));
			}

			public static void Assign(object instance, IServiceProvider provider)
			{
				if (instance == null)
					throw new ArgumentNullException("instance");
				if (provider == null)
					throw new ArgumentNullException("provider");

				_assignMethods.Get(instance.GetType())(provider, instance);
			}

			private delegate void AssignDelegate(IServiceProvider provider, object instance);
		}
	}
}
